import { Component, type StyledProps } from '@knno/jsx';
import { defineElement } from '../defineElement';
import { DATA } from '../symbol';

const RouterElement = defineElement('knno-router-view', HTMLDivElement, 'div');

export type TargetComponent = ((path: string) => Promise<Node> | Node) | Node;
type Interception = (() => Promise<Node> | Node) | Node;

export interface Rule {
	/**
	 * The path to match.
	 */
	path: string;
	/**
	 * The target component to render.
	 * If it is a function, it will be called with the path as the parameter.
	 * If it is a node, it will be rendered directly.
	 * @example
	 * {
	 * 	path: '/',
	 * 	target: () => <div>Home</div>
	 * }
	 */
	component: TargetComponent;
	/**
	 * Whether this rule is a prefix match.
	 * If true, the rule will be matched when the path starts with the rule path.
	 * If false, the rule will be matched when the path is exactly the same as the rule path.
	 * @default false
	 */
	prefixMatch?: boolean;
}

interface MatchItem {
	[DATA]?: { path: string; target: TargetComponent; prefixMatch: boolean };
	[index: string]: MatchItem;
}

function setRule(
	item: MatchItem,
	arr: string[],
	path: string,
	target: TargetComponent,
	prefixMatch: boolean,
) {
	if (arr.length > 0) {
		let k = item[arr[0]];
		if (!k) {
			k = {};
			item[arr[0]] = k;
		}
		setRule(k, arr.slice(1), path, target, prefixMatch);
	} else {
		item[DATA] = { path, target, prefixMatch };
	}
}

export type RouterViewProps = StyledProps<RouterView> & {
	/**
	 * Current rendered path
	 */
	path?: string;
	rules?: Rule[];
	onRender?: (
		this: RouterView,
		hash: string,
		matchedRule: { path: string; isPrefixMatch: boolean } | null,
	) => Interception | undefined;
	onConnected?: (this: RouterView) => void;
	onDisconnected?: (this: RouterView) => void;
};

export class RouterView extends Component<RouterViewProps> {
	private [DATA]: MatchItem = {};

	constructor(props: RouterViewProps) {
		super(props);
		if (props.rules) this.addRules(props.rules);
	}

	protected render(): Node {
		return (
			<RouterElement
				class={this.props.class}
				style={this.props.style}
				onConnected={() => {
					this.props.onConnected?.call(this);
				}}
				onDisconnected={() => {
					this.props.onDisconnected?.call(this);
				}}
			/>
		);
	}

	get rules(): Rule[] {
		return this.props.rules ?? [];
	}
	setRules(rules: Rule[]) {
		this[DATA] = {};
		this.addRules(rules);
		return this;
	}

	addRules(rules: Rule[]) {
		rules.forEach((r) => {
			let path = r.path;
			if (!path.startsWith('/')) {
				path = '/' + path;
			}
			if (path.endsWith('/')) {
				path = path.substring(0, path.length - 1);
			}
			const arr = path.split('/');
			setRule(this[DATA], arr, r.path, r.component, r.prefixMatch ?? false);
		});
		this.setPath(this.path);
		return this;
	}

	private renderInterception(
		hash: string,
		matchedRule: { path: string; isPrefixMatch: boolean } | null,
	): boolean {
		const interception = this.props.onRender?.call(this, hash, matchedRule);
		if (interception) {
			if (typeof interception === 'function') {
				Promise.resolve(interception()).then((value) => {
					this.el.replaceChildren(value);
				});
				return true;
			}
			this.el.replaceChildren(interception);
			return true;
		} else {
			return false;
		}
	}

	get path(): string {
		return this.props.path ?? '';
	}
	setPath(path: string): this {
		this.props.path = path;
		if (path.startsWith('#')) {
			path = path.substring(1);
		}
		if (!path.startsWith('/')) {
			path = '/' + path;
		}
		if (path.endsWith('/')) {
			path = path.substring(0, path.length - 1);
		}
		const arr = path.split('/');
		let item: MatchItem = this[DATA];
		let prefixItem: MatchItem | undefined;
		for (const p of arr) {
			let find = item[p];
			if (!find) {
				find = item['*'];
				if (!find) {
					item = find;
					break;
				} else {
					item = find;
				}
			} else {
				item = find;
			}
			if (item[DATA]?.prefixMatch) {
				prefixItem = item;
			}
		}
		if (!item && prefixItem) {
			item = prefixItem;
		}
		if (item) {
			const comp = item[DATA];
			if (comp && typeof comp.target === 'function') {
				if (this.renderInterception(path, { path: comp.path, isPrefixMatch: comp.prefixMatch })) {
					return this;
				}
				Promise.resolve(comp.target(path)).then((value) => {
					this.el.replaceChildren(value);
				});
			} else if (comp && typeof comp.target !== 'function') {
				if (this.renderInterception(path, { path: comp.path, isPrefixMatch: comp.prefixMatch })) {
					return this;
				}
				this.el.replaceChildren(comp.target);
			} else {
				if (this.renderInterception(path, null)) {
					return this;
				}
				this.el.innerHTML = '';
			}
		} else {
			if (this.renderInterception(path, null)) {
				return this;
			}
			this.el.innerHTML = '';
		}
		return this;
	}

	onRender(
		handler: (
			this: RouterView,
			hash: string,
			matchedRule: { path: string; isPrefixMatch: boolean } | null,
		) => Interception | undefined,
	): this {
		this.props.onRender = handler;
		return this;
	}

	onConnected(handler: (this: RouterView) => void): this {
		this.props.onConnected = handler;
		return this;
	}

	onDisConnected(handler: (this: RouterView) => void): this {
		this.props.onDisconnected = handler;
		return this;
	}
}
